(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
這一篇要討論的是function component的效能問題
在上一篇中,我們發現即使MenuItem接收的props並沒有被改變,MenuItem的return元素也和context
無關,但是它在Menu的button按下時還是被重新渲染了。
import React, {useContext} from 'react';
import { OpenContext } from '../context/ControlContext';
const menuItemStyle = {
marginBottom: "7px",
paddingLeft: "26px",
listStyle: "none"
};
function MenuItem(props){
const isOpenUtil = useContext(OpenContext);
return <li style={menuItemStyle}>{props.text}</li>;
}
export default MenuItem;
function Menu(props){
const isOpenUtil = useContext(OpenContext);
return (
<div style={menuContainerStyle}>
<p style={menuTitleStyle}>{props.title}</p>
<button style={menuBtnStyle} onClick={
()=>{isOpenUtil.setOpenContext(!isOpenUtil.openContext)}
}>
{(isOpenUtil.openContext)?"^":"V"}
</button>
<ul>{props.children}</ul>
</div>
);
}
import React, {useState} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording=[
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
];
const MenuPage = () =>{
const [isOpen, setIsOpen] = useState(true);
/* 請注意這一行的位置 */
let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
return (
<OpenContext.Provider value={{
openContext: isOpen,
setOpenContext: setIsOpen
}} >
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
</OpenContext.Provider>
);
}
export default MenuPage;
在上一篇的最後,我們雖然是這樣說的:
會造成這個問題是因為useContext原始的實作方法,不是去檢查該context更動會不會改變子元件。而是當context被更新時,直接讓所有使用useContext的元件都被重新渲染。
然而如果你把引入useContext的那行註解掉,會發現元件依然有被重複渲染。
import React from 'react';
//import { OpenContext } from '../context/ControlContext';
const menuItemStyle = {
marginBottom: "7px",
paddingLeft: "26px",
listStyle: "none"
};
function MenuItem(props){
//const isOpenUtil = useContext(OpenContext);
return <li style={menuItemStyle}>{props.text}</li>;
}
export default MenuItem;
請注意MenuPage使用MenuItem的這一行
let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
如果你把這一行寫在const MenuPage = () =>{}
裡面,即使你沒有引入Context也會有重複渲染的問題。
import React, {useState,useCallback} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording=[
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
];
const MenuPage = () =>{
const [isOpen, setIsOpen] = useState(true);
let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
return (
<OpenContext.Provider value={{
openContext: isOpen,
setOpenContext: setIsOpen
}} >
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
</OpenContext.Provider>
);
}
export default MenuPage;
這是因為我們前面說過,React的function component在每次重新渲染時都會呼叫整個Component函式的定義域。如果你把wording加工成MenuItem的map寫在MenuPage中,當MenuPage被重新渲染時,就會重新呼叫一次這個「把wording加工成MenuItem的map」。
現在我們只是自己開發所有程式,可以透過把「加工元件的函式」拉到function component外解決,但在與別人合作時,就無法保證同事會怎麼寫。此時就要思考一件事:
換句話說,我們現在需要的就是一個「會幫我們檢查需不需要改變子元件」的中介層。
High Order Component並不是一個元件的類別,它是一種特別的設計觀念:
「 用來加工 Component 的 function 」
意思是說,這類的函式的作用就是讓你把Component丟進去給它,它會幫你加工成一個新的Component。
const ComponentNew = HOCFunction(ComponentOld);
memo
就是React提供的「會幫我們檢查元件需不需要重新渲染」的中介層的一個HOC。
被memo
產生出的新元件會記憶住上一次元件的props值,當父元件被重新渲染,子元件沒有變動、但父元件又想要渲染它時,memo
會去比較該子元件的props有沒有和前一次記憶的結果不同,如果有才重新渲染該子元件。
現在,讓我們從React函式庫中引入memo
。
import React, {memo} from 'react';
並在檔案的最後,讓export
出去的Component用memo()
包起來
import React, {memo} from 'react';
const menuItemStyle = {
marginBottom: "7px",
paddingLeft: "26px",
listStyle: "none"
};
function MenuItem(props){
return <li style={menuItemStyle}>{props.text}</li>;
}
export default memo(MenuItem);
現在我們再次用React-dev-tool去檢查我們的製作的元件:
就會發現MenuItem的確沒有被渲染了。
雖然這個問題可以透過memo
解決,但是請不要把memo
當萬靈丹。因為你加了一層memo
,React就要多做一件事情。當遇到像這個case有其他解決方法時(移到function component外去加工元件),我們就不應該讓程式多一個負擔。
我們會用大約四到五篇先來討論如何解決function component本身的效能問題,再回頭討論該如何處理useContext本身的效能問題。